/**
 * Copyright (c) 2012, Hakans Pirates - 
 * Johan Andersson spaiki17@gmail.com, 
 * Mattias Lundberg lundberg.mattias@gmail.com, 
 * Samuel Sjodin sjoodin@gmail.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the <organization> nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *  
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package pirates.ship;

import java.util.Iterator;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.bullet.collision.PhysicsCollisionListener;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.font.BitmapText;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

import pirates.Pirates;
import pirates.PiratesServer;
import pirates.Settings;
import pirates.crates.CrateControl;
import static pirates.Settings.*;
import pirates.crates.FloatingCrate;
import pirates.inputDevices.ManualControl;
import pirates.network.messages.CannonBallMessage;
import pirates.network.messages.CrateMessage;
import pirates.notifications.Notify;
import pirates.ship.cannon.BallControl;
import pirates.ship.cannon.CannonBall;

/**
 * 
 * @author Johan
 */
public class ShipControl extends RigidBodyControl
        implements
        ManualControl,
        PhysicsCollisionListener {

    public static final Quaternion YAW090 = new Quaternion().fromAngleAxis(
            FastMath.PI / 2, new Vector3f(0, 1, 0));
    public static final Quaternion YAW270 = new Quaternion().fromAngleAxis(
            FastMath.PI * 3 / 2, new Vector3f(0, 1, 0));
    private float velocity = 0;
    private float accelerate = 0;
    private float steerValue = 0;
    private float steer = 0;
    private float aim = 0;
    private boolean sunk = false;
    private SimpleApplication app;
    private PhysicsSpace phySpace;
    private ShipState state;
    private boolean debug = false;
    private BitmapText debugText;
    private Node debugNode;

    public ShipControl(CollisionShape shape, float mass, SimpleApplication app,
            PhysicsSpace phySpace, ShipState state) {
        super(shape, mass);
        this.app = app;
        this.phySpace = phySpace;
        this.state = state;
        setKinematic(true);
    }

    public void accelerate(float value, float tpf) {
        accelerate = value;
    }

    public void steer(float value, float tpf) {
        steerValue = value;
    }

    public void predict(Vector3f position, float velocity, Quaternion rotation,
            float angularVelocity, float time) {

        this.velocity = velocity;
        this.steer = angularVelocity;

        spatial.setLocalTranslation(position);
        spatial.setLocalRotation(rotation);

    }

    @Override
    public void update(float tpf) {
        if (sunk) {
            spatial.move(0, -2, 0);
            if (getPhysicsLocation().y < -180) {
                app.getRootNode().detachChild(spatial);
            }
        } else {

            calculateVelocity(tpf);
            calculateSteer(tpf);

            spatial.rotate(new Quaternion().fromAngleAxis(steer * tpf * 50,
                    new Vector3f(0, 1, 0)));
            spatial.move(getDirection().multLocal(velocity * tpf * 50, 0,
                    velocity * tpf * 50));

            // if(spatial.getName().equals(Settings.HOST_NAME)){
            // Notify.displayValueDebug(app, "Name: ", Settings.HOST_NAME);
            //
            // calculateVelocity(tpf);
            // calculateSteer(tpf);
            //
            // spatial.rotate(new Quaternion().fromAngleAxis(steer, new
            // Vector3f(0, 1, 0)));
            // spatial.move(getDirection().multLocal(velocity, 0, velocity));
            //
            // } else {
            //
            // calculateVelocity(tpf);
            // calculateSteer(tpf);
            //
            // spatial.rotate(new Quaternion().fromAngleAxis(steer, new
            // Vector3f(0, 1, 0)));
            // spatial.move(getDirection().multLocal(velocity, 0, velocity));
            //
            // calculateVelocity(tpf);
            // calculateSteer(tpf);
            //
            // spatial.setLocalRotation(info.rotation.mult(new
            // Quaternion().fromAngleAxis(steer, new Vector3f(0, 1, 0))));
            //
            // }
        }

        if (debug) {
            Notify.displayValueDebug(app, "Position:", spatial.getLocalTranslation().toString());
            Notify.displayValueDebug(app, "Speed:", velocity);
            Notify.displayValueDebug(app, "Steer:", steer);
        }

        if (Settings.HOST_NAME.equals(spatial.getName())
                && Settings.SCENARIO_HAS_ENDED) {
            Notify.gameEndInformation(app,
                    spatial.<String>getUserData(ShipConstants.TEAM + "") + "");

        } else if ((Settings.HOST_NAME.equals(spatial.getName()))) {
            Notify.playerInformation(app, spatial.<Float>getUserData(ShipConstants.HITPOINTS + ""),
                    spatial.<Float>getUserData(ShipConstants.MAX_HITPOINTS + ""),
                    spatial.<Integer>getUserData(ShipConstants.CANNON_BALLS + ""),
                    spatial.<Integer>getUserData(ShipConstants.MAX_CANNON_BALLS + ""),
                    spatial.<String>getUserData(ShipConstants.TEAM + "") + "");
        }
        super.update(tpf);
    }

    public float getVelocity() {
        return velocity;
    }

    public float getSteer() {
        return steer;
    }

    private void calculateVelocity(float tpf) {
        if (sunk) {
            velocity = 0;
            return;
        }
        float maxspeed = spatial.<Float>getUserData(ShipConstants.MAX_SPEED + "");
        float acceleration = spatial.<Float>getUserData(ShipConstants.ACCELERATION
                + "");
        float deacceleration = spatial.<Float>getUserData(ShipConstants.DEACCELERATION
                + "");

        velocity += tpf
                * (acceleration * accelerate - (FastMath.sign(velocity)
                * FastMath.sqr(velocity) * deacceleration));
        if (FastMath.abs(velocity) > maxspeed) {
            velocity = maxspeed * accelerate;
        }

    }

    private void calculateSteer(float tpf) {
        if (sunk) {
            steer = 0;
            return;
        }
        float max_acceleration = spatial.<Float>getUserData(ShipConstants.MAX_ANGULAR_ACCELERATION + "");
        float acceleration = spatial.<Float>getUserData(ShipConstants.ANGULAR_ACCELERATION + "");
        float deacceleration = spatial.<Float>getUserData(ShipConstants.ANGULAR_DEACCELERATION + "");

        steer += tpf
                * ((steerValue * acceleration) - (FastMath.sign(steer)
                * FastMath.sqr(steer) * deacceleration));

        if (steer > max_acceleration) {
            steer = max_acceleration;
        } else if (steer < -max_acceleration) {
            steer = -max_acceleration;
        }
    }

    public Vector3f getPosition() {
        return getPhysicsLocation();
    }

    public Vector3f getDirection() {
        return getPhysicsRotationMatrix().mult(Vector3f.UNIT_X);
    }

    public Vector3f getHeight() {
        int h = spatial.<Integer>getUserData(ShipConstants.SHOOT_HEIGHT + "");
        return new Vector3f(0, (float) h, 0);
    }

    public int getRadius() {
        return spatial.<Integer>getUserData(ShipConstants.SHOOT_RADIUS + "");
    }

    public void aim(float value, float tpf) {
        this.aim += value * tpf;
        if (this.aim < 0) {
            this.aim = 0;
        }
        if (this.aim > .25f) {
            this.aim = .25f;
        }
    }

    public void shoot(int i) {
        int balls = spatial.<Integer>getUserData(ShipConstants.CANNON_BALLS + "");

        if (balls-- <= 0) {
            return;
        } else {
            spatial.setUserData(ShipConstants.CANNON_BALLS + "", balls);
        }

        if (i == -1) {
            Vector3f dir = YAW270.toRotationMatrix().mult(getDirection()).normalize();

            Quaternion aimQ = new Quaternion().fromAngleAxis(
                    -aim * FastMath.PI, getDirection().normalize());

            CannonBall c = new CannonBall(app, this, phySpace, app.getRootNode());

            c.shoot(getPosition().add(dir.mult(getRadius()).add(getHeight())),
                    aimQ.toRotationMatrix().mult(dir));

            CannonBallMessage msg = new CannonBallMessage(Settings.HOST_NAME,
                    getPosition().add(dir.mult(getRadius()).add(getHeight())),
                    aimQ.toRotationMatrix().mult(dir), c.getName());
            ((Pirates) app).myClient.send(msg);

        } else {
            Vector3f dir = YAW090.toRotationMatrix().mult(getDirection()).normalize();

            Quaternion aimQ = new Quaternion().fromAngleAxis(aim * FastMath.PI,
                    getDirection().normalize());

            CannonBall c = new CannonBall(app, this, phySpace, app.getRootNode());

            c.shoot(getPosition().add(dir.mult(getRadius()).add(getHeight())),
                    aimQ.toRotationMatrix().mult(dir));

            CannonBallMessage msg = new CannonBallMessage(Settings.HOST_NAME,
                    getPosition().add(dir.mult(getRadius()).add(getHeight())),
                    aimQ.toRotationMatrix().mult(dir), c.getName());
            ((Pirates) app).myClient.send(msg);
        }
    }

    public void hitByBall(float ballWeight) {
        float hitpoints = spatial.<Float>getUserData(ShipConstants.HITPOINTS + "");

        hitpoints -= ballWeight / 10f;

        if (hitpoints <= 0) {
            sink();
        }

        spatial.setUserData(ShipConstants.HITPOINTS + "", hitpoints);
    }

    public void sink() {
        if (!sunk) {
            sunk = true;
            space.removeCollisionObject(this);
            int b = spatial.<Integer>getUserData(ShipConstants.CANNON_BALLS + "");
            drop(b, 0);
            if (Settings.IS_SERVER && !Settings.SCENARIO_HAS_ENDED) { // check
                // sunk
                Iterator<Spatial> i = ((PiratesServer) app).ships.getAIships();
                boolean allIsSunked = true;
                while (i.hasNext()) {
                    Spatial aiShip = i.next();
                    if (!aiShip.getControl(ShipControl.class).isSunked()) {
                        allIsSunked = false;
                    }
                }
                if (allIsSunked) { // if all tradeships has sunk
                    ((PiratesServer) app).stopScenarioAndDeclareWinner(EntityTeam.PIRATES);
                }

            }
        }
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    public void collision(PhysicsCollisionEvent event) {
        Notify.displayValueDebug(app, "Collision ", event.getLocalPointB().subtract(event.getLocalPointA()).toString());

        if (event.getObjectA() instanceof CrateControl
                || event.getObjectB() instanceof CrateControl) {
            return;
        }
        if (event.getObjectA() instanceof BallControl
                || event.getObjectB() instanceof BallControl) {
            return;
        }
        
        if (!Settings.IS_SERVER) {
            if (event.getNodeA() != null
                    && event.getNodeA().getName().equals(Settings.HOST_NAME) && 
                    event.getNodeB() != null) {
                processCollision(event.getNodeA(), event.getNodeB());
            } else if (event.getNodeB() != null
                    && event.getNodeB().getName().equals(Settings.HOST_NAME) && 
                    event.getNodeA() != null) {
                processCollision(event.getNodeB(), event.getNodeA());
            }
        }

    }

    private void processCollision(Spatial you, Spatial him) {
        Vector3f collisionVector = (you.getLocalTranslation().subtract(him.getLocalTranslation())).mult(Settings.COLLISION_FACTOR);
        you.move(collisionVector.x, 0, collisionVector.z);
        velocity = 0;
        steer = 0;
    }

    public void pickup(FloatingCrate crate) {
        // Add balls
        if (!crate.pickedup) {
            crate.pickedup = true;
        } else {
            return;
        }

        int balls = spatial.<Integer>getUserData(ShipConstants.CANNON_BALLS + "");
        int maxBalls = spatial.<Integer>getUserData(ShipConstants.MAX_CANNON_BALLS + "");
        int pickup = crate.getNumCannonBalls();

        if (balls + pickup > maxBalls) {
            balls = maxBalls;
        } else {
            balls += pickup;
        }
        spatial.setUserData(ShipConstants.CANNON_BALLS + "", balls);

        // Add health
        float health = spatial.<Float>getUserData(ShipConstants.HITPOINTS + "");
        float maxHealth = spatial.<Float>getUserData(ShipConstants.MAX_HITPOINTS + "");
        int pickupH = crate.getNumHealth();

        if (health + pickupH > maxHealth) {
            health = maxHealth;
        } else {
            health += pickupH;
        }
        spatial.setUserData(ShipConstants.HITPOINTS + "", health);

        if (!Settings.IS_SERVER) {
            Notify.debug("Picked up " + crate.name);
            CrateMessage msg = new CrateMessage(crate.name, crate.id,
                    Vector3f.ZERO, 0, 0, true, Settings.HOST_NAME);
            ((Pirates) app).myClient.send(msg);
        }
    }

    // Drop when key pressed.
    public void drop() {
        drop(5, 5);
    }

    public void drop(int dropB, int dropH) {
        float health = spatial.<Float>getUserData(ShipConstants.HITPOINTS + "");
        if (health - dropH < 0) {
            return;
        }

        int balls = spatial.<Integer>getUserData(ShipConstants.CANNON_BALLS + "");
        if (balls - dropB < 0) {
            return;
        }

        spatial.setUserData(ShipConstants.HITPOINTS + "", health - dropH);
        spatial.setUserData(ShipConstants.CANNON_BALLS + "", balls - dropB);

        Vector3f loc = getPosition().add(
                getDirection().normalize().mult(-110));
        loc.x += 1;


        CrateMessage msg = new CrateMessage(Settings.HOST_NAME
                + FloatingCrate.nextId++, 0, loc, dropH, dropB,
                false, Settings.HOST_NAME);
        if (Settings.IS_SERVER) {
        } else {
            ((Pirates) app).myClient.send(msg);
        }
    }

    boolean isSunked() {
        return sunk;
    }
}
